[#50]: Build Prepare for Offline chapter selection#175
Conversation
Add project picker, chapter selection accordion with per-book grids, and a dedicated prepare-offline query module to avoid conflicts with in-flight recording PRs.
Sync role-filtered chapter assignments from the web My Work endpoint, auto-repair stale local data on launch, and show a cloud indicator when download sync is needed. Refactors Prepare for Offline components and Settings/header layout.
fel-cesar
left a comment
There was a problem hiding this comment.
Solid, well-decomposed work on #50 — the prepare-offline/* components and usePrepareOfflineSelection are clean and well-tested.
Blockers
Question regarding changes on the sync-repair heuristic (can never resolve for an unassigned project member).
Minor navigation issue:
Once we select a project, there is no way of going back to project selection, once we tab in the back button, we go back to settings page.
Kapture.2026-07-15.at.10.43.42.mp4
| async function syncUserChapterWork(userId: number) { | ||
| return retrySyncStep( | ||
| 'User chapter work sync', | ||
| KV_KEYS.SYNC_ERROR_CHAPTER_ASSIGNMENTS, |
There was a problem hiding this comment.
Both syncChapterAssignments and syncUserChapterWork write their retrySyncStep error state to the same KV key, so a failure in one step overwrites the other's error status and muddies sync-error reporting. Consider a dedicated key (e.g. SYNC_ERROR_USER_CHAPTER_WORK).
"Same" error for both cases. If that is intentional or not important, consider it non blocking.
| export async function userNeedsAssigneeRepair( | ||
| userId: number, | ||
| ): Promise<boolean> { | ||
| const db = getDatabase(); | ||
| const result = await db.execute( | ||
| `SELECT | ||
| COUNT(*) AS total, | ||
| SUM( | ||
| CASE | ||
| WHEN ca.assigned_user_id IS NOT NULL OR ca.peer_checker_id IS NOT NULL THEN 1 | ||
| ELSE 0 | ||
| END | ||
| ) AS with_role | ||
| FROM chapter_assignments ca | ||
| INNER JOIN project_units pu ON pu.id = ca.project_unit_id | ||
| INNER JOIN user_projects up ON up.project_id = pu.project_id | ||
| WHERE up.user_id = ?`, | ||
| [userId], | ||
| ); | ||
| const row = result.rows?.[0] as | ||
| | { total: number; with_role: number } | ||
| | undefined; | ||
| const total = Number(row?.total ?? 0); | ||
| const withRole = Number(row?.with_role ?? 0); | ||
| return total > 0 && withRole === 0; | ||
| } |
There was a problem hiding this comment.
question:
userNeedsAssigneeRepair still uses assigned_user_id IS NOT NULL OR peer_checker_id IS NOT NULL (any user, not scoped to userId) → still can never resolve for an unassigned member
DB is shared across users on the device (the "Add user" feature), if some other user previously synced and populated their assignee rows in the same project, then with_role > 0 and the current unassigned user looks "healthy." So the bug specifically bites a user who has no assignments and whose projects have no assignee rows from any other device user — e.g. the common single-user-with-no-assignments case.
What are your thoughts on this? (should we heavy test for multi user approach here as well?)
TLDR
Implements Prepare for Offline chapter selection (#50) and fixes My Work showing empty after sync by pulling role-filtered assignments from the same endpoint the web app uses. Adds a cloud icon state and auto-sync when local assignment data is incomplete.
Reviewer checklist
Closes #NNNwhen this PR completes it)AGENTS.md)Details
Closes #50
Prepare for Offline (#50): Project picker when opened from Settings, collapsible chapter accordion grouped by book, per-book select-all, and a 5-column chapter grid. Assigned users get pre-selected assigned chapters with a collapsed accordion; unassigned users start with an empty selection and expanded accordion. Optional
{ projectId, projectName }route params for future #39 trigger.My Work sync fix: Mobile previously synced only
GET /users/:id/chapter-assignments/all, which returns project chapters without assignee fields populated. Web My Work usesGET /users/:id/chapter-assignments(role-filtered). Sync now calls both endpoints;userNeedsAssigneeRepair()forces a full re-sync when local chapters exist but no assignee/checker is set. My Work filter includesnot_startedanddraftstages for assignees.Cloud sync UX: New
online_needs_syncstatus shows an amber refresh overlay on the cloud icon when download sync is needed. Home auto-triggers sync once per session when incomplete local data is detected while online.UI polish: Settings screen OFFLINE section layout, shared header safe-area padding, status-bar/header fixes on stack screens.
Type of change:
Technical changes
src/db/queries.prepareOffline.ts— project chapters withassigned_user_idsrc/hooks/usePrepareOfflineSelection.ts— selection state, assigned/unassigned init, accordion defaultssrc/app/prepare-offline/—ProjectPickerStep,ChapterSelectionAccordion,BookChapterSection,BookSectionHeader,ChapterSelectorGrid,SelectionCheckboxsrc/services/sync.ts—syncUserChapterWork(), assignee repair on stale local datasrc/services/api.ts—getUserChapterAssignments()src/db/repository.ts—userNeedsAssigneeRepair()src/hooks/useLocalSyncHealth.ts— detects incomplete local assignment datasrc/utils/syncStatusState.ts—online_needs_syncstatussrc/components/ui/CloudSyncStatusIcon.tsx— amber refresh glyph for updates availablesrc/app/screens/HomeScreen.tsx— auto-sync when local data needs repairLines changed: ~1,500 | Files modified: 42
Testing
npm run format:check— passnpm run lint— passnpm run typecheck— passnpm test -- --ci— 179 passedNew tests:
groupChaptersByBook.test.ts,usePrepareOfflineSelection.test.ts,ChapterSelectorGrid.test.tsx,PrepareForOfflineScreen.test.tsx,useLocalSyncHealth.test.ts,syncStatusState.test.ts(needs-sync cases),mapChapterAssignment.test.tsHow to verify
npm run format:check && npm run lint && npm run typecheck && npm test -- --cimrace+t@gloo.uson dev) → confirm My Work populates without manually tapping sync; cloud icon briefly shows amber then syncsExpected: My Work shows assigned chapters after launch sync; cloud icon reflects stale data; chapter selection UI matches #50 scope. No download button or resource tiers (#51 deferred).
Follow-ups
{ projectId, projectName }route params; picker skipped when params present